package com.dbflow5.processor.definition;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.ForeignKey;
import com.dbflow5.annotation.ManyToMany;
import com.dbflow5.annotation.PrimaryKey;
import com.dbflow5.annotation.Table;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

/**
 * Description: Generates the Model class that is used in a many to many.
 */
public class ManyToManyDefinition extends BaseDefinition {

    public TypeName databaseTypeName;

    private final TypeName referencedTable;
    private final boolean generateAutoIncrement;
    private final boolean sameTableReferenced;
    private final String generatedTableClassName;
    private final boolean saveForeignKeyModels;
    private final String thisColumnName;
    private final String referencedColumnName;

    public ManyToManyDefinition(TypeElement element, ProcessorManager processorManager, ManyToMany manyToMany) {
        super(element, processorManager);

        databaseTypeName = ProcessorUtils.extractTypeNameFromAnnotation(element, Table.class, table -> {
            table.database();
            return null;
        });

        referencedTable = ProcessorUtils.extractTypeNameFromAnnotation(manyToMany, e -> null, manyToMany1 -> {
            manyToMany1.referencedTable();
            return null;
        });

        generateAutoIncrement = manyToMany.generateAutoIncrement();
        sameTableReferenced = referencedTable == elementTypeName;
        generatedTableClassName = manyToMany.generatedTableClassName();
        saveForeignKeyModels = manyToMany.saveForeignKeyModels();
        thisColumnName = manyToMany.thisTableColumnName();
        referencedColumnName = manyToMany.referencedTableColumnName();

        if (!com.dbflow5.processor.utils.StringUtils.isNullOrEmpty(thisColumnName) && !com.dbflow5.processor.utils.StringUtils.isNullOrEmpty(referencedColumnName) && thisColumnName.equals(referencedColumnName)) {
            manager.logError(ManyToManyDefinition.class, "The thisTableColumnName and referenceTableColumnName cannot be the same");
        }
    }

    public void prepareForWrite() {
        if (!generatedTableClassName.isEmpty()) {
            ClassName referencedOutput = ElementExtensions.toClassName(ElementExtensions.toTypeElement(referencedTable, manager), manager);
            setOutputClassName("_" + referencedOutput.simpleName());
        } else {
            setOutputClassNameFull(generatedTableClassName);
        }
    }

    @Override
    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {
        typeBuilder.addAnnotation(AnnotationSpec.builder(Table.class)
                .addMember("database", "$T.class", databaseTypeName).build());

        TableDefinition referencedDefinition = manager.getTableDefinition(databaseTypeName, referencedTable);
        TableDefinition selfDefinition = manager.getTableDefinition(databaseTypeName, elementTypeName);

        if (generateAutoIncrement) {
            AnnotationSpec.Builder annnotationBuilder = AnnotationSpec.builder(PrimaryKey.class).addMember("autoincrement", CodeBlock.of("true"));
            typeBuilder.addField(FieldSpec.builder(TypeName.LONG, "_id").addAnnotation(annnotationBuilder.build()).build());

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getId").addModifiers(Modifier.PUBLIC, Modifier.FINAL).returns(TypeName.LONG).addStatement("return _id");
            typeBuilder.addMethod(methodBuilder.build());
        }

        if(referencedDefinition != null) {
            appendColumnDefinitions(typeBuilder, referencedDefinition, 0, referencedColumnName);
        }

        if(selfDefinition != null) {
            appendColumnDefinitions(typeBuilder, selfDefinition, 1, thisColumnName);
        }
    }

    @Override
    public TypeName extendsClass() {
        return ClassNames.BASE_MODEL;
    }

    private void appendColumnDefinitions(TypeSpec.Builder typeBuilder, TableDefinition referencedDefinition, int index, String optionalName) {
        String fieldName = referencedDefinition.elementName.toLowerCase();
        if (sameTableReferenced) {
            fieldName += index;
        }
        // override with the name (if specified)
        if (optionalName.isEmpty()) {
            fieldName = optionalName;
        }

        FieldSpec.Builder fieldBuilder = FieldSpec.builder(referencedDefinition.elementClassName, fieldName);
        if(!generateAutoIncrement) {
            fieldBuilder.addAnnotation(AnnotationSpec.builder(PrimaryKey.class).build());
        }
        fieldBuilder.addAnnotation(AnnotationSpec.builder(ForeignKey.class).addMember("saveForeignKeyModel", CodeBlock.of(String.valueOf(saveForeignKeyModels))).build());
        typeBuilder.addField(fieldBuilder.build());


        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("get" + StringUtils.capitalize(fieldName))
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .returns(referencedDefinition.elementClassName)
                .addStatement("return " + fieldName);
        typeBuilder.addMethod(methodBuilder.build());

        MethodSpec.Builder methodBuilder2 = MethodSpec.methodBuilder("set" + StringUtils.capitalize(fieldName))
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addParameter(referencedDefinition.elementClassName, "param")
                .addStatement(fieldName + " = param");
        typeBuilder.addMethod(methodBuilder2.build());
    }
}